package org.benetech.ref;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.benetech.beans.Filter;
import org.benetech.beans.FilterCallback;
import org.benetech.beans.NameOrIdMatchesFilter;
import org.jdom.Attribute;
import org.jdom.Element;


public final class XML {

	public static class Regex {
		/** Matches h1...h6. Group 1 provides the heading # */
        public static final Pattern HEADINGS_PATTERN = Pattern.compile("h([1-6])");

        /** Matches level1...level6. Group 2 provides the level # */
		public static final Pattern LEVEL_PATTERN = Pattern.compile("(level)([0-9])");

        /** Matches p */
        public static final Pattern PARAGRAPH_PATTERN = Pattern.compile("p");

		/**
		 * Matches a string with a number in it. Group 1 provides the text before the number, group 2 the number,
		 * and group 3 the text after the number.
         */
        public static final Pattern NUMBER_PATTERN = Pattern.compile("([.[^0-9]]*)([0-9]+)([.[^0-9]]*)");
        public static final Pattern ENTITY_PATTERN = Pattern.compile("\\&([a-zA-Z0-9]+);");
		public static final Pattern DOCTYPE_PATTERN = Pattern.compile("<![Dd][Oo][Cc][Tt][Yy][Pp][Ee]");
		public static final Pattern DIV_SPAN_PATTERN = Pattern.compile("div|span");
		public static final Pattern TD_PATTERN = Pattern.compile("td");
		public static final Pattern LINK_PATTERN = Pattern.compile("a");
		/**
         * These must contain text to be valid. Otherwise they can be safely removed.
         */
        public static final Pattern TEXT_CONTAINING_PATTERN = Pattern.compile("div|span|p|li|h[1-6]+");

        public static final Pattern PAGENUM_PATTERN = Pattern.compile("pagenum");

		public static final Pattern PAGEBREAK_PATTERN = Pattern.compile("pagebreak");

		public static final Pattern FRONT_MATTER_PATTERN = Pattern.compile("[fF][rR][oO][nN][tT][mM][aA][tT][tT][eE][rR]");
		public static final Pattern BODY_MATTER_PATTERN = Pattern.compile("[bB][oO][dD][yY][mM][aA][tT][tT][eE][rR]");
		public static final Pattern REAR_MATTER_PATTERN = Pattern.compile("[rR][eE][aA][rR][mM][aA][tT][tT][eE][rR]");
		public static final Pattern REAR_MATTER_PATTERN2 = Pattern.compile("[bB][aA][cC][kK][mM][aA][tT][tT][eE][rR]");
		public static final Pattern BOOK_PATTERN = Pattern.compile("book");
		public static final Pattern HR_PATTERN = Pattern.compile("hr");
		public static final Pattern LIST_PATTERN = Pattern.compile("(ol|ul)");
		public static final Pattern B_PATTERN = Pattern.compile("b");
		public static final Pattern BR_PATTERN = Pattern.compile("(br)");
		public static final Pattern SUP_PATTERN = Pattern.compile("(sup)");
		// XXX
		//public static final Pattern TEXT_CONTAINING_PATTERN = Pattern.compile(XML.Regex.TEXT_CONTAINING_PATTERN + "|td");
		//private static final
	}

	public static class Find {

		/**
         * Accepts links.
         */
        public static final Filter<Element, Matcher> LINK_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.LINK_PATTERN);
        /**
         * Accepts headings from level 1..6.
         */
        public static final Filter<Element, Matcher> HEADING_FILTER = new NameOrIdMatchesFilter(true,
        		XML.Regex.HEADINGS_PATTERN);
		/**
         * Accepts levels from level 1..6.
         */
        public static final Filter<Element, Matcher> LEVEL_FILTER = new NameOrIdMatchesFilter(true,
        		XML.Regex.LEVEL_PATTERN);
		/**
         * Accepts paragraphs.
         */
        public static final Filter<Element, Matcher> PARAGRAPH_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.PARAGRAPH_PATTERN);
		/**
         * Accepts divs and spans.
         */
        public static final Filter<Element, Matcher> DIV_SPAN_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.DIV_SPAN_PATTERN);
		/**
         * Accepts paragraphs.
         */
        public static final Filter<Element, Matcher> BR_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.BR_PATTERN);
		/**
         * Accepts table cells.
         */
        public static final Filter<Element, Matcher> TD_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.TD_PATTERN);
        /**
         * Accepts page breaks.
         */
        public static final Filter<Element, Matcher> PAGEBREAK_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.PAGEBREAK_PATTERN);
        /**
         * Accept page numbers.
         */
        public static final Filter<Element, Matcher> PAGENUM_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.PAGENUM_PATTERN);

        /**
         * Accept sup elements.
         */
        public static final Filter<Element, Matcher> SUP_FILTER = new NameOrIdMatchesFilter(true,
        		Regex.SUP_PATTERN);

		/**
         * Takes multiple elements, returns true if they all have the same name.
         */
        public static final Filter<Element, ? > MATCHING_NAME_FILTER = new Filter<Element, Object>() {
        	public boolean accept(final Element... elements) {
        		for (int i = 1; i < elements.length; ++i) {
        			if (!elements[0].getName().equals(elements[i].getName())) {
        			    return false;
        			}
        		}
        		return elements.length != 0;
        	}

        	public boolean acceptWithCallback(final FilterCallback callback, final Element... elements) {
        		return accept(elements);
        	}
        };

		/**
         * Takes a single element, returns true if it is a span with one letter.
         */
        public static final Filter<Element, ? > SPAN_WITH_NO_SPACE_ON_END = new Filter<Element, Object>() {
        	public boolean accept(final Element... elements) {
        		if (elements.length != 1) {
        			return false;
        		}
        		return (elements[0].getName().equals("span") && elements[0].getText().length() > 0
        				&& elements[0].getText().length() > 0
        				&& elements[0].getText().charAt(elements[0].getText().length() - 1) != ' ');
        	}

        	public boolean acceptWithCallback(final FilterCallback callback, final Element... elements) {
        		return accept(elements);
        	}
        };

		/**
         * Takes a single element, returns true if it is a span with one letter.
         */
        public static final Filter<Element, ? > SPAN_WITH_NO_SPACE_ON_BEGINING = new Filter<Element, Object>() {
        	public boolean accept(final Element... elements) {
        		if (elements.length != 1) {
        			return false;
        		}
        		return (elements[0].getName().equals("span") && elements[0].getText().length() > 0
        				&& elements[0].getText().length() > 0
        				&& elements[0].getText().charAt(0) != ' ');
        	}

        	public boolean acceptWithCallback(final FilterCallback callback, final Element... elements) {
        		return accept(elements);
        	}
        };


		/**
         * Takes a single element, returns true if it is a span or div with no attributes.
         */
        public static final Filter<Element, ? > SPAN_OR_DIV_WITH_NO_ATTRIBUTES_FILTER = new Filter<Element, Object>() {
        	public boolean accept(final Element... elements) {
        		if (elements.length != 1) {
        			return false;
        		}
        		return (elements[0].getName().equals("div") || elements[0].getName().equals("span"))
        		        && (elements[0].getAttributes().size() == 0);
        	}

        	public boolean acceptWithCallback(final FilterCallback callback, final Element... elements) {
        		return accept(elements);
        	}
        };

		/**
         * Looks for BRs outside containers. Of course it's a candidate for generalization.
         */
        public static final Filter<Element, ?> BR_OUTSIDE_CONTAINER_FILTER = new Filter<Element, Object>() {
	        public boolean accept(final Element... elements) {
		        for (final Element element : elements) {
			        final String name = element.getName();
			        final String parentName = element.getParentElement().getName();
			        final Matcher brNameMatcher = Regex.BR_PATTERN.matcher(name);
			        if (brNameMatcher.matches()) {
				        final Matcher textElementMatcher = Regex.TEXT_CONTAINING_PATTERN.matcher(parentName);
				        return !textElementMatcher.matches();
			        }
		        }
		        return false;
	        }

	        public boolean acceptWithCallback(final FilterCallback callback, final Element... elements) {
		        return accept(elements);
	        }
        };

        /**
         * Finds frontmatter, bodymatter, rearmatter. Takes one element at a time.
         * Forces the element name to lower case
         */
        public static final Filter<Element, ? > MATTER_FILTER = new Filter<Element, Object>() {
	        public boolean accept(final Element... elements) {
		        if (elements.length != 1) {
			        return false;
		        }
		        final Element element = elements[0];
		        final List<Attribute> attributes = (List<Attribute>) element.getAttributes();
		        for (Attribute attribute : attributes) {
			        final Matcher front = Regex.FRONT_MATTER_PATTERN.matcher(attribute.getValue());
			        if (!front.matches()) {
				        final Matcher body = Regex.BODY_MATTER_PATTERN.matcher(attribute.getValue());
				        if (!body.matches()) {
					        final Matcher rear = Regex.REAR_MATTER_PATTERN.matcher(attribute.getValue());
					        final Matcher rear2 = Regex.REAR_MATTER_PATTERN2.matcher(attribute.getValue());
					        if (rear.matches() || rear2.matches()) {
						        element.setName("rearmatter");
						        return true;
					        }
				        } else {
					        element.setName("bodymatter");
					        return true;
				        }
			        } else {
				        element.setName("frontmatter");
				        return true;
			        }
		        }
		        return false;
	        }

	        public boolean acceptWithCallback(final FilterCallback callback, final Element... elements) {
		        return accept(elements);
	        }
        };

        /**
         * Accepts named anchors.
         */
        public static final Filter<Element, ? > ANCHOR_NAMED_FILTER = new Filter<Element, Object>() {
			public boolean accept(Element... items) {
				for (Element e : items) {
					if (!(e.getName().equals("a") && e.getAttribute("name") != null)) {
						return false;
					}
				}
				return true;
            }

			public boolean acceptWithCallback(FilterCallback<Object> callback, Element... items) {
				return accept(items);
			}
        };

        /**
         * Accepts attributes in anchor tags that are not href or name.
         */
        public static final Filter<Attribute, ? > ANCHOR_OTHER_ATTRIBUTE_FILTER = new Filter<Attribute, Object>() {
			public boolean accept(final Attribute... attributes) {
				if (attributes.length != 1) {
					return false;
				}
				final Attribute attribute = attributes[0];
				return !(attribute.getName().equals("href") || attribute.getName().equals("name"));
			}
			public boolean acceptWithCallback(final FilterCallback callback, final Attribute... attributes) {
				return accept(attributes);
			}
		};
	}
}
